/*
 *  Copright: 2008 Philipp Leitner, Distributed Systems Group,
 *            TU Wien, leitner@infosys.tuwien.ac.at
 *
 *  This file is part of DAIOS.
 *
 *  DAIOS is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as
 *  published by the Free Software Foundation.
 * 
 *  DAIOS is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with DAIOS.  If not, see
 *  <http://www.gnu.org/licenses/>.
 * 
 */

package at.ac.tuwien.infosys.dsg.daiosPlugins.axis2.axiom;

import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;

import javax.xml.namespace.QName;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMElement;

import at.ac.tuwien.infosys.dsg.daios.exceptions.ArrayException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.TypeErrorException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.UnsupportedClassException;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IConstants;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosInputMessage;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosMessage;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosOutputMessage;
import at.ac.tuwien.infosys.dsg.daios.wsdl.AtomicTypesMapper;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLMessage;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLOperation;
import at.ac.tuwien.infosys.dsg.daios.wsdl.interfaces.IWSDLPart;
import at.ac.tuwien.infosys.dsg.daios.wsdl.typesystem.WSDLTypeTree;

/**
 * A concrete subclass of OMElementConverter. This is the concrete
 * instance used to convert messages in 'rpc/encoded' style invocations.
 * 
 * <p>
 * 
 * This converter transforms Daios Messages to AXIOM models (as
 * used by Apache Axis2) and vice versa.
 * 
 * @author philipp
 *
 */
public class OMRPCConverter extends OMElementConverter {
	
	/**
	 * Private constructor. Create a new converter instance to
	 * a given WSDL output message.
	 * 
	 */
	private OMRPCConverter() {
		log = Logger.getLogger(IConstants.LOG_NAME);
		fac = OMAbstractFactory.getOMFactory();
	}
	
	/**
	 * The factory method used to construct new instances of SOAPRPCConverter.
	 */	
	protected static OMRPCConverter getInstance() {
		return new OMRPCConverter();
	}
	
	/**
	 * Converts a Daios message to AXIOM using the given
	 * WSDL operation and WSDL input message.
	 * 
	 * @param op The WSDL operation to use in the invocation.
	 * @param msg The WSDL message to use in the invocation.
	 * @param actualMsg The Daios message that provides the
	 * acutal user-delivered input data.
	 * @param types A tree containing all types from the WSDL
	 * definition.
	 */
	public OMElement convert(IWSDLOperation op, IWSDLMessage msg,
			DaiosInputMessage actualMsg, WSDLTypeTree types) {
		
		// create root element (the operation name)
		OMElement root = fac.createOMElement(op.getName(),
			null);
		
		// now iterate over all parts
		List<IWSDLPart> parts = msg.getParts();
		for(IWSDLPart part: parts)
			if(actualMsg.get(part.getPartName()) != null) {
				
				// for every part we check if it is a
				// - simple type
				// - array type
				// - or complex type
				
				boolean isSimple = actualMsg.isSimple(part.getPartName());
				boolean isArray = actualMsg.isArray(part.getPartName());
				
				// add simple type
				if(isSimple && !isArray)
					root.addChild(
						addOMPart(part.getPartName(),
							AtomicTypesMapper.encodeForWire(
								actualMsg.get(part.getPartName())),
							null,null));
		
				// add array type
				else if(isSimple && isArray)
					root.addChild(
						addOMPartFromArray(part.getPartName(),
								actualMsg.get(part.getPartName()),
								null,null));
		
				// add complex type
				else if(!isSimple && !isArray) {
					try {
						root.addChild(
							addOMPart(part.getPartName(),
								actualMsg.getComplex(part.getPartName()),
								null,null));
					} catch (TypeErrorException e) {
						log.severe("Internal Daios error while processing "+
							"your request.");
						e.printStackTrace();
					}
				}
				// add array of complex types
				else {
					try {
						OMElement array = fac.createOMElement(part.getPartName(),
												null);
						for(DaiosMessage m:
							actualMsg.getComplexArray(part.getPartName())) {
							
							array.addChild(
								addOMPart(part.getPartType(),m,
									null,null));

						}
						root.addChild(array);
					} catch (TypeErrorException e) {
						log.severe("Internal Daios error while processing "+
							"your request.");
						e.printStackTrace();
					}
				}
			}
		
		return root;
	}

	/**
	 * Converts a XML response encoded as AXIOM model
	 * into a DaiosOutputMessage.
	 * 
	 * <p>
	 * 
	 * Note that we do not need the additional information
	 * from knowing the WSDL output message in rpc/encoded,
	 * since the type information is already 'encoded' (therefore
	 * rpc/<i>encoded</i>) in the SOAP message.
	 * 
	 * @param returnMsg The XML response as OM model.
	 * @return The XML document converted to a DaiosOutputMessage.
	 */
	public DaiosOutputMessage convert(OMElement returnMsg) {
		
		DaiosOutputMessage m = new DaiosOutputMessage();
		List<OMElement> multiref = extractMultirefs(returnMsg);
		Iterator it = returnMsg.getChildren();
		Object o = null;
		OMElement el = null;
		OMAttribute href = null;
		OMAttribute type = null;
		OMAttribute arraytype = null;
		while(it.hasNext()) {
			o = it.next();
			if(o instanceof OMElement)
				el = (OMElement) o;
			else {
				// this is whitespace (hopefully :-) )
				continue;
			}
			href = (OMAttribute) el.getAttribute(new QName("href"));
			type = (OMAttribute) el.getAttribute(
					new QName(IConstants.XSI_NS, "type"));
			// we have no reference and no array, i.e. a simple type
			if(href == null && !type.getAttributeValue().endsWith("Array")) {
				extractSimpleValue(m, el, type);
			}
			// we have no reference and an array, i.e. an array type
			else if(href == null && type.getAttributeValue().endsWith("Array")) {
				arraytype = (OMAttribute) el.getAttribute(
						new QName(IConstants.SOAPENC_NS, "arrayType"));
				extractArrayValues(el,m,parseSizeFromAtt(arraytype));
			}
			// we have a complex type
			else if(href != null && !type.getAttributeValue().endsWith("Array"))
				extractComplexValue(m,el,href,multiref);
			// we have an array of complex types
			else
				extractComplexArrayValues(m,el,href,multiref);
					
		}
		
		return m;

	}
	
	/**
	 * Convert an asynchronously received document into a Daios message.
	 * For some reason we sometimes receive different messages from
	 * Axis2 depending on whether we use synchronous or asynchronous
	 * invocations. This might be a bug in Axis2.
	 * 
	 * @param result The asynchronous result to convert
	 * @return The converted Daios message.
	 */
	public DaiosOutputMessage convertAsync(OMElement result) {
		DaiosOutputMessage m = new DaiosOutputMessage();
		
		OMElement body = OMUtil.getBody(result);
		List<OMElement> multiref = extractMultirefs(body);
		
		OMElement returnMsg = null;
		
		Iterator it = body.getChildren();
		while(it.hasNext()) {
			returnMsg = (OMElement) it.next();
			if(!returnMsg.getLocalName().equals("multiRef"))
				break;
		}
		
		OMElement subelement = null;
		OMAttribute href = null;
		OMAttribute type = null;
		OMAttribute arraytype = null;
		it = returnMsg.getChildren();
		while(it.hasNext()) {
			subelement = (OMElement) it.next();
			href = subelement.getAttribute(new QName("href"));
			type = (OMAttribute) subelement.getAttribute(
						new QName(IConstants.XSI_NS, "type"));
			arraytype = subelement.getAttribute(
					new QName(IConstants.SOAPENC_NS, "arrayType"));
			boolean isSimple = checkIfSimple(subelement);
			boolean isArray = checkIfArray(subelement);
			// we have no href and no array, i.e. a simple type
			if(isSimple && !isArray) {
				extractSimpleValue(m,subelement,type);
			}
			// we have no reference and an array, i.e. an array type
			else if(isSimple && isArray) {
				extractArrayValues(subelement,m,parseSizeFromAtt(arraytype));
			}
			// a complex type
			else if(!isSimple && !isArray)
				m.setComplex(subelement.getLocalName(),
					getComplexType(href,multiref));
			// an array of complex types
			else
				extractComplexArrayValues(m,subelement,href,multiref);
		}		
		
		return m;
		
	}
	
	/**
	 * Extract a list of all 'multiref' elements from an RPC
	 * SOAP invocation response. These multiref elements contain
	 * the actual complex return types.
	 * 
	 * @param body The body of a SOAP message.
	 * @return A list of all multirefs contained in this SOAP
	 * body.
	 */
	private List<OMElement> extractMultirefs(OMElement body) {
		List<OMElement> multiref = 
			new LinkedList<OMElement>();
		Iterator it = body.getChildrenWithName(new QName("multiRef"));
		while(it.hasNext())
			multiref.add((OMElement)it.next());
		return multiref;
	}
	
	/**
	 * Get a complex type identified by its 'href' name from a list
	 * of all multirefs and convert it to a Daios message.
	 * 
	 * @param href The identifier of the complex type to fetch.
	 * @param multiref The list of all refs.
	 * @return The complex type identified by href as Daios message.
	 */
	private DaiosMessage getComplexType(OMAttribute href,
			List<OMElement> multiref) {
		
		String hrefS = href.getAttributeValue();
		hrefS = hrefS.substring(1, hrefS.length());
		
		// get the relevant multiref
		OMElement ref = null;
		for(OMElement om: multiref) {
			if(om.getAttributeValue(new QName("id")).equals(hrefS)) {
				ref = om;
				break;
			}
		}
		
		// add these fields to a message
		DaiosMessage m = new DaiosMessage();
		
		Iterator it = ref.getChildren();
		OMElement child = null;
		OMAttribute nextHref = null;
		OMAttribute type = null;
		Object o = null;
		while(it.hasNext()) {
			o = it.next();
			if(!(o instanceof OMElement))
				continue;
			child = (OMElement) o;
			// as always: we have received a simple type
			type = (OMAttribute) child.getAttribute(
				new QName(IConstants.XSI_NS, "type"));
			if((nextHref = child.getAttribute(new QName("href"))) == null) {
				try {
					String val = child.getText();
					Class vClass =
						AtomicTypesMapper.get(type.getAttributeValue());
					Object castedVal
						= AtomicTypesMapper.construct(val,vClass);
					m.set(child.getLocalName(),castedVal,vClass);
				}
				catch(Exception e) {
					e.printStackTrace();
				}
			}
			// there is again a complex type
			else
				m.setComplex(child.getLocalName(),
					getComplexType(nextHref,multiref));
		}
		
		return m;
	}
	
	/**
	 * Extract a simple value from the XML model and insert it into
	 * the output message.
	 * 
	 * @param m The output message to insert into.
	 * @param el The simple value as AXIOM model.
	 * @param type The type name of the simple type (as encoded in the SOAP
	 * message).
	 */
	private void extractSimpleValue(DaiosOutputMessage m, OMElement el,
			OMAttribute type) {
		
		try {
			
			String val = el.getText();
			Class vClass =
				AtomicTypesMapper.get(type.getAttributeValue());
			Object castedVal
				= AtomicTypesMapper.construct(val,vClass);
			m.set(el.getLocalName(),castedVal,vClass);
			
		} catch (IOException e) {
			log.severe("Internal Daios error while processing the "+
				"server response.");
			e.printStackTrace();
		} catch (UnsupportedClassException e) {
			log.severe("Internal Daios error while processing the "+
			"server response.");
		e.printStackTrace();
		} catch (TypeErrorException e) {
			log.severe("Internal Daios error while processing the "+
			"server response.");
		e.printStackTrace();
		} catch (ArrayException e) {
			log.severe("Internal Daios error while processing the "+
			"server response.");
		e.printStackTrace();
		}
	}

	/**
	 * Extract an array of simple values from the XML model and insert them
	 * as array value into the output message.
	 * 
	 * @param parent The parent OM element that contains all array values.
	 * @param m The output message to insert into.
	 * @param size The number of array elements contained in parent.
	 */
	private void extractArrayValues(OMElement parent, DaiosOutputMessage m,
			int size) {
		
		Iterator it = parent.getChildren();
		OMElement el = (OMElement)it.next();
		String type = el.getAttributeValue(
				new QName(IConstants.XSI_NS, "type"));
		
		try {
			
			Class arrayclass = AtomicTypesMapper.get(type);
			Object array = Array.newInstance(arrayclass, size);
			String val = el.getText();
			Object castedVal
				= AtomicTypesMapper.construct(val,arrayclass);
			int i = 0;
			Array.set(array,i++,castedVal);
			while(it.hasNext()) {
				el = (OMElement) it.next();
				val = el.getText();
				castedVal = AtomicTypesMapper.construct(val,arrayclass);
				Array.set(array,i++,castedVal);
			}
			
			m.set(parent.getLocalName(), array);
			
		} catch (IOException e) {
			log.severe("Internal Daios error while processing the "+
				"server response.");
			e.printStackTrace();
		} catch (UnsupportedClassException e) {
			log.severe("Internal Daios error while processing the "+
				"server response.");
			e.printStackTrace();
		} catch (ArrayException e) {
			log.severe("Internal Daios error while processing the "+
				"server response.");
			e.printStackTrace();
		}
		
	}
	
	/**
	 * Extract a complex value from the XML model and insert it
	 * as DaiosMessage into the output message.
	 * 
	 * <p>
	 * 
	 * This does not work because of a bug in Axis2. See
	 * 
	 * <link>https://issues.apache.org/jira/browse/AXIS2-2408</link>.
	 * 
	 * @param m The output message to insert into.
	 * @param el The complex value as AXIOM model.
	 * @param href A reference name identifying the complex type.
	 * @param multiref A list of all multirefs.
	 */		
	private void extractComplexValue(DaiosOutputMessage m, OMElement el,
			OMAttribute href, List<OMElement> multiref) {
		throw new RuntimeException("See Axis2 Bug \n"+
				"https://issues.apache.org/jira/browse/AXIS2-2408");
	}
	
	/**
	 * Extract an array of complex values from the XML model and insert them
	 * as array of DaiosMessages into the output message.
	 * 
	 * @param m The output message to insert into.
	 * @param el The complex array value as AXIOM model.
	 * @param href A reference name identifying the complex type.
	 * @param multiref A list of all multirefs.
	 */
	private void extractComplexArrayValues(DaiosOutputMessage m, OMElement el,
			OMAttribute href, List<OMElement> multiref) {

		List<DaiosMessage> l = new LinkedList<DaiosMessage>();
		
		// go down to the actual array values
		el = el.getFirstElement();
		
		String name = el.getLocalName();

		DaiosOutputMessage arrayElement = null;
		
		// get all array values in a list
		for(OMElement e = el; e != null && e.getLocalName().equals(name);
			e = OMUtil.getNextOMElement(e)) {
			
			OMElement ref = getRefFromList(e, multiref);
			arrayElement = convert(ref);
			l.add(arrayElement);
			
		}
		
		// now add this array to the message
		DaiosMessage[] messages = convert(l);
		try {
			m.setComplexArray(name, messages);
		} catch (ArrayException e) {
			log.severe("Internal Daios error while processing the "+
				"server response.");
			e.printStackTrace();
		}

		
	}
	
	/**
	 * 
	 * @param el
	 * @param refs
	 */
	private OMElement getRefFromList(OMElement el, List<OMElement> refs) {
		OMAttribute href = el.getAttribute(new QName("href"));
		String myRef = href.getAttributeValue().replaceFirst("#", "");
		for(OMElement thisEl : refs)
			if(thisEl.getAttribute(new QName("id")).getAttributeValue().equals(myRef)) {
				return thisEl;
			}
		return null;
	}
	
	/**
	 * Check if a certain AXIOM construct represents a simple value.
	 * 
	 * <p>
	 * 
	 * We consider a value 'simple' if it is not null,
	 * has no 'href' element (pointing to a multiref element)
	 * and does not have 'complex' children.
	 * 
	 * 
	 * @param el The element to check.
	 * @return True if the element is simple, false otherwise.
	 */
	private boolean checkIfSimple(OMElement el) {
		
		if(el == null)
			return true;
		
		if(el.getAttribute(new QName("href")) != null)
			return false;
		if(!checkIfSimple(el.getFirstElement()))
			return false;
		else
			return true;
		
		
	}
	
	/**
	 * Check if a certain AXIOM construct represents an array value.
	 * 
	 * <p>
	 * 
	 * An element is considered an 'array' if it has a
	 * well-defined array type.
	 * 
	 * @param el The element to check.
	 * @return True if the element is an array, false otherwise.
	 */
	private boolean checkIfArray(OMElement el) {
		
		if(el == null)
			return false;
		
		return getArrayType(el) != null;
		
	}	
	
	/**
	 * Parse the array type from an array parent element
	 * in AXIOM notion.
	 * 
	 * <p>
	 * 
	 * This is a little a hack: we assume that attribute types
	 * are given as TYPE[SIZE], e.g. xsd:int[5] for a array of
	 * 5 int values. We therefore replace the [XX] part of the
	 * array type to get the actual type without the size.
	 * 
	 * @param el The array parent element.
	 * @return The array type as encoded in the SOAP message. 
	 */
	private String getArrayType(OMElement el) {
		
		OMAttribute arrayType =
			el.getAttribute(
					new QName(IConstants.SOAPENC_NS, "arrayType"));
		
		if(arrayType == null)
			return null;
		
		// TODO this probably won't always work
		return arrayType.getAttributeValue().replaceFirst("\\[.*\\]", "");
		
	}
	
	/**
	 * Parse the array size from a 'size' attribute.
	 * 
	 * <p>
	 * 
	 * This is a little a hack: we assume that attribute types
	 * are given as TYPE[SIZE], e.g. xsd:int[5] for a array of
	 * 5 int values. We therefore cut out the number between the
	 * brackets to figure out the array size.
	 * 
	 * @param arraytype The array type attribute.
	 * @return The size as encoded in the attribute.
	 */
	private int parseSizeFromAtt(OMAttribute arraytype) {
		
		String val = arraytype.getAttributeValue();
		
		val = val.replaceAll(".*\\Q[\\E","");
		val = val.replaceAll("\\Q]\\E","");
		
		return Integer.parseInt(val);
		
	}

}
